04 JS 深拷贝
- 深拷贝(deep copy)是指完全复制一个对象及其所有子对象,使得新对象与原对象互相独立,修改新对象不会影响原对象 (新对象与原对象不共享任何内存空间。)。
- 相对于浅拷贝(shallow copy),深拷贝不仅复制对象本身,还递归地复制所有嵌套的子对象。
JSON 方法
最简单且常用的方法是利用 JSON.stringify 和 JSON.parse。这是最简单的一种实现深拷贝的方法,但它有一些限制:
- 不能拷贝函数、
undefined、Symbol。 - 不能正确处理 Date 对象(会被转换成字符串)。
- 不能处理正则表达式对象。
- 不能处理循环引用。
js
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = deepClone(obj1);
console.log(obj2 !== obj1); // true
console.log(obj2.b !== obj1.b); // true
obj2.b.c = 3;
console.log(obj1.b.c); // 输出:2递归手动拷贝
递归函数可以处理大多数情况下的深拷贝,包括数组和对象。
WARNING
在深拷贝时,如果不处理循环引用,递归调用会进入无限循环,最终导致栈溢出错误。
js
function deepClone(obj, hash = new WeakMap()) {
// 基本类型(如字符串、数字)或 null 直接返回
if (obj === null) return null;
if (typeof obj !== 'object') return obj;
// 处理日期对象,创建新的实例返回
if (obj instanceof Date) return new Date(obj);
// 处理正则表达式对象,创建新的实例返回
if (obj instanceof RegExp) return new RegExp(obj);
// 处理循环引用,若已存在于 hash 中则返回记录的副本
// 使用 WeakMap 跟踪已经拷贝的对象,避免循环引用导致的无限递归
if (hash.has(obj)) return hash.get(obj);
// 创建新的对象或数组
// 使用 obj.constructor 创建与原对象相同类型的新实例,并将其存入 WeakMap
let cloneObj = new obj.constructor();
hash.set(obj, cloneObj);
// 遍历对象的所有属性
// 遍历对象的所有自身属性(不包括原型链上的属性),递归调用 deepClone 进行拷贝
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
// 递归拷贝属性
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
// 测试代码
const obj1 = {
a: 1,
b: { c: 2 },
d: new Date(),
e: /test/i,
f: function() { return 'hello'; },
g: [1, 2, 3]
};
obj1.self = obj1; // 创建循环引用,用于测试
const obj2 = deepClone(obj1);
console.log(obj2);
console.log(obj2 !== obj1); // true
console.log(obj2.b !== obj1.b); // true
obj2.b.c = 3;
console.log(obj1.b.c); // 输出:2使用第三方库
使用第三方库,如 Lodash 提供的 _.cloneDeep 方法,它是一个广泛使用的工具函数,可以处理大多数深拷贝的场景。
js
const _ = require('lodash');
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = _.cloneDeep(obj1);
console.log(obj2 !== obj1); // true
console.log(obj2.b !== obj1.b); // true
obj2.b.c = 3;
console.log(obj1.b.c); // 输出:2现代浏览器的结构化克隆算法(structuredClone)
structuredClone 是一种内置的方法,专门用于深拷贝。
与 JSON.stringify() 和 JSON.parse() 方法相比,structuredClone 有以下优势:
- 它能够处理循环引用。
- 它能够正确地复制
Date对象、RegExp对象、Map、Set、Blob、FileList、ImageBitmap和ImageData等特殊对象。 - 它保留了对象的类型信息,不需要像
JSON方法那样转换对象类型。 - 它的性能通常比
JSON方法要好,因为它不需要将对象序列化和反序列化。
基本用法
structuredClone 的一些特性:
- 不可转移的值:如果对象中包含不可转移的值(如函数、Symbol、Promise、WeakMap、WeakSet 等),则
structuredClone会抛出异常。 - 循环引用:
structuredClone能够正确处理循环引用的情况。 - 传输数据:
structuredClone可以用于在不同的执行上下文(例如,不同的 Web Workers)之间传输数据。
js
const original = {
num: 0,
str: 'string',
bool: true,
nul: null,
und: undefined,
obj: { id: 'object' },
arr: [0, 1, 2],
date: new Date(),
reg: new RegExp('/reg/'),
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
};
const clone = structuredClone(original);
console.log(clone); // { num: 0, str: 'string', bool: true, nul: null, und: undefined, obj: { id: 'object' }, arr: [0, 1, 2], date: 2023-10-10T16:00:00.000Z, reg: /\/reg\//, map: Map(1) { 'key' => 'value' }, set: Set(3) { 1, 2, 3 } }
console.log(clone !== original); // true
console.log(clone.b !== original.b); // true
console.log(clone.d !== original.d); // true处理循环引用
js
const obj = {};
obj.self = obj; // 循环引用
const clone = structuredClone(obj);
console.log(clone.self === clone); // true,说明循环引用被正确处理